package com.evancharlton.mileage.dao;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.List;

import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.util.Log;

import com.evancharlton.mileage.exceptions.InvalidFieldException;
import com.evancharlton.mileage.provider.FillUpsProvider;

/**
 * A base data access object (DAO). Exposes/provides the common functionality
 * such as persisting objects.
 */
public abstract class Dao implements Cloneable {
	private static final String TAG = "Dao";

	public static final String _ID = BaseColumns._ID;

	public static final String TIMESTAMP = "last_change";

	@Column(type = Column.LONG, name = BaseColumns._ID)
	private long mId;

	@Column(type = Column.TIMESTAMP, name = TIMESTAMP)
	private long mTimestamp;

	private Uri mUriBase = null;

	private boolean mInMemoryDataChanged = false;

	protected Dao(final ContentValues values) {
		load(values);
	}

	public Dao(final Cursor cursor) {
		load(cursor);
	}

	public Object clone() {
		try {
			return super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
		return null;
	}

	public void load(Cursor cursor) {
		if (cursor.isBeforeFirst()) {
			cursor.moveToFirst();
		}
		mId = cursor.getLong(cursor.getColumnIndex(_ID));

		// automagically populate based on @Column annotation definitions
		Field[] fields = getClass().getDeclaredFields();
		for (Field field : fields) {
			Column column = field.getAnnotation(Column.class);
			if (column != null) {
				int columnIndex = cursor.getColumnIndex(column.name());
				Object value = null;
				switch (column.type()) {
					case Column.BOOLEAN:
						value = cursor.getInt(columnIndex);
						if (value == null) {
							value = new Boolean(column.value() == 1);
						} else {
							value = ((Integer) value).intValue() == 1;
						}
						break;
					case Column.DOUBLE:
						value = cursor.getDouble(columnIndex);
						if (value == null) {
							value = new Double(column.value());
						}
						break;
					case Column.INTEGER:
						value = cursor.getInt(columnIndex);
						if (value == null) {
							value = new Integer(column.value());
						}
						break;
					case Column.LONG:
						value = cursor.getLong(columnIndex);
						if (value == null) {
							value = new Long(column.value());
						}
						break;
					case Column.STRING:
						value = cursor.getString(columnIndex);
						if (value == null) {
							value = "";
						}
						break;
					case Column.TIMESTAMP:
						Long ms = cursor.getLong(columnIndex);
						if (ms != null) {
							value = new Date(ms);
						} else {
							value = new Date(System.currentTimeMillis());
						}
						break;
				}
				if (value != null) {
					try {
						field.set(this, value);
					} catch (IllegalArgumentException e) {
						Log.e(TAG, "Couldn't set value for " + field.getName(), e);
					} catch (IllegalAccessException e) {
						Log.e(TAG, "Couldn't access " + field.getName(), e);
					}
				}
			}
		}
	}

	// TODO(3.1) - Remove this code duplication.
	public void load(ContentValues values) {
		if (values == null) {
			mId = -1;
			return;
		}
		Long id = values.getAsLong(_ID);
		if (id == null) {
			mId = -1;
		} else {
			mId = id.longValue();
		}

		// automagically populate based on @Column annotation definitions
		Field[] fields = getClass().getDeclaredFields();
		for (Field field : fields) {
			Column column = field.getAnnotation(Column.class);
			if (column != null) {
				Object value = null;
				switch (column.type()) {
					case Column.BOOLEAN:
						value = values.getAsBoolean(column.name());
						if (value == null) {
							value = new Boolean(column.value() == 1);
						}
						break;
					case Column.DOUBLE:
						value = values.getAsDouble(column.name());
						if (value == null) {
							value = new Double(column.value());
						}
						break;
					case Column.INTEGER:
						value = values.getAsInteger(column.name());
						if (value == null) {
							value = new Integer(column.value());
						}
						break;
					case Column.LONG:
						value = values.getAsLong(column.name());
						if (value == null) {
							value = new Long(column.value());
						}
						break;
					case Column.STRING:
						value = values.getAsString(column.name());
						if (value == null) {
							value = "";
						}
						break;
					case Column.TIMESTAMP:
						Long ms = values.getAsLong(column.name());
						if (ms != null) {
							value = new Date(ms);
						} else {
							value = new Date(System.currentTimeMillis());
						}
						break;
				}
				if (value != null) {
					try {
						field.set(this, value);
					} catch (IllegalArgumentException e) {
						Log.e(TAG, "Couldn't set value for " + field.getName(), e);
					} catch (IllegalAccessException e) {
						Log.e(TAG, "Couldn't access " + field.getName(), e);
					}
				}
			}
		}
	}

	/**
	 * Get the URI for this instance of a DAO.
	 * 
	 * @return the URI for the DAO instance.
	 */
	public Uri getUri() {
		if (mUriBase == null) {
			DataObject annotation = getClass().getAnnotation(DataObject.class);
			mUriBase = Uri.withAppendedPath(FillUpsProvider.BASE_URI, annotation.path());
		}
		if (isExistingObject()) {
			return ContentUris.withAppendedId(mUriBase, getId());
		}
		return mUriBase;
	}

	/**
	 * Validate the data object (intended to be done before saving). If there is
	 * an invalid field value, throw an InvalidFieldException
	 * 
	 * @return the ContentValues to be passed to persistent storage.
	 * @throws InvalidFieldException in the event of a validation error
	 */
	protected final void validate(ContentValues values) throws InvalidFieldException {
		preValidate();
		Field[] fields = getClass().getDeclaredFields();
		for (Field field : fields) {
			Validate validate = field.getAnnotation(Validate.class);
			if (validate == null) {
				continue;
			}
			int errorMessage = validate.value();
			try {
				Object value = field.get(this);
				if (validate != null) {
					if (errorMessage > 0) {
						// see if it's null when it shouldn't be
						if (value == null && field.getAnnotation(Nullable.class) != null) {
							throw new InvalidFieldException(errorMessage);
						}

						// check strings
						if (value instanceof String && field.getAnnotation(CanBeEmpty.class) == null) {
							if (((String) value).length() == 0) {
								throw new InvalidFieldException(errorMessage);
							}
						}

						// check the numeric types
						if (value instanceof Number) {
							boolean checkPast = field.getAnnotation(Past.class) != null;
							boolean checkPositive = field.getAnnotation(Range.Positive.class) != null;
							if (value instanceof Double) {
								if (checkPositive && ((Double) value) <= 0D) {
									throw new InvalidFieldException(errorMessage);
								}
							}

							if (value instanceof Long) {
								if (checkPositive && ((Long) value) <= 0L) {
									throw new InvalidFieldException(errorMessage);
								}
								if (checkPast && ((Long) value) >= System.currentTimeMillis()) {
									throw new InvalidFieldException(errorMessage);
								}
							}

							if (value instanceof Integer) {
								if (checkPositive && ((Integer) value) <= 0) {
									throw new InvalidFieldException(errorMessage);
								}
							}
						}
					}

					// we made it without any errors (or no validation needed)
					Column column = field.getAnnotation(Column.class);
					if (column != null) {
						String data = null;
						if (value instanceof Date) {
							data = String.valueOf(((Date) value).getTime());
						} else if (value instanceof Boolean) {
							data = ((Boolean) value).booleanValue() ? "1" : "0";
						} else {
							data = String.valueOf(value);
						}
						values.put(column.name(), data);
					}
				}
			} catch (IllegalArgumentException e) {
				Log.e(TAG, e.getMessage(), e);
			} catch (IllegalAccessException e) {
				Log.e(TAG, e.getMessage(), e);
			}
		}
	}

	protected void preValidate() {
	}

	public boolean save(Context context) throws InvalidFieldException {
		ContentValues values = new ContentValues();
		validate(values);
		values.put(TIMESTAMP, System.currentTimeMillis());
		if (isExistingObject()) {
			// update
			values.put(_ID, mId);
			context.getContentResolver().update(getUri(), values, null, null);
		} else {
			// insert
			Uri uri = context.getContentResolver().insert(getUri(), values);
			List<String> segments = uri.getPathSegments();
			String id = segments.get(segments.size() - 1);
			mId = Long.parseLong(id);
		}
		return true;
	}

	public boolean saveIfChanged(Context context) throws InvalidFieldException {
		if (mInMemoryDataChanged) {
			return save(context);
		}
		return false;
	}

	public boolean delete(Context context) {
		return context.getContentResolver().delete(getUri(), null, null) > 0;
	}

	public final boolean isExistingObject() {
		return mId > 0;
	}

	public final long getId() {
		return mId;
	}

	public final void setId(long id) {
		mId = id;
	}

	protected void setInMemoryDataChanged() {
		mInMemoryDataChanged = true;
	}

	protected long getLong(Cursor cursor, String columnName) {
		return cursor.getLong(cursor.getColumnIndex(columnName));
	}

	protected double getDouble(Cursor cursor, String columnName) {
		return cursor.getDouble(cursor.getColumnIndex(columnName));
	}

	protected String getString(Cursor cursor, String columnName) {
		return cursor.getString(cursor.getColumnIndex(columnName));
	}

	protected boolean getBoolean(Cursor cursor, String columnName) {
		return cursor.getInt(cursor.getColumnIndex(columnName)) == 1;
	}

	protected int getInt(Cursor cursor, String columnName) {
		return cursor.getInt(cursor.getColumnIndex(columnName));
	}

	protected int getInt(ContentValues values, String key, int defaultValue) {
		Integer value = values.getAsInteger(key);
		if (value != null) {
			return value.intValue();
		}
		return defaultValue;
	}

	protected String getString(ContentValues values, String key, String defaultValue) {
		String value = values.getAsString(key);
		if (value != null) {
			return value;
		}
		return defaultValue;
	}

	protected double getDouble(ContentValues values, String key, double defaultValue) {
		Double value = values.getAsDouble(key);
		if (value != null) {
			return value.doubleValue();
		}
		return defaultValue;
	}

	protected boolean getBoolean(ContentValues values, String key, boolean defaultValue) {
		Boolean value = values.getAsBoolean(key);
		if (value != null) {
			return value.booleanValue();
		}
		return defaultValue;
	}

	protected long getLong(ContentValues values, String key, long defaultValue) {
		Long value = values.getAsLong(key);
		if (value != null) {
			return value.longValue();
		}
		return defaultValue;
	}

	// TODO: make this a series of annotations instead?
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Column {
		public static final int STRING = 0;
		public static final int INTEGER = 1;
		public static final int DOUBLE = 2;
		public static final int BOOLEAN = 3;
		public static final int TIMESTAMP = 4;
		public static final int LONG = 5;

		int value() default 0;

		int type();

		String name();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.TYPE)
	public @interface DataObject {
		String path();
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Validate {
		int value() default 0;
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Nullable {
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface CanBeEmpty {
	}

	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Past {
	}

	public static class Range {
		@Retention(RetentionPolicy.RUNTIME)
		@Target(ElementType.FIELD)
		public @interface Positive {
		}
	}
}